Android修炼:Lottie是如何解放了开发的双手?
↓推荐关注↓
Lottie 是 Airbnb 开发的一套跨平台框架,可以将 AE 生成的动画效果,在各个平台呈现出来,支持 Android、iOS、Web、MacOS、Windows 等。以 Android 为例,设计师使用 AE 设计好动画,通过插件 Bodymovin 将 AE 工程文件导出为 json 文件,app 通过 Lottie 解析出相应的数据结构,最后通过 Canvas 进行绘制。
如果平时自己测试玩,可以直接在 LottieFiles 网站 去下载 json 动画文件,内容丰富。看个栗子吧,效果见下:
此动画效果可通过 AE 导出为 RobotWave.json 文件,详见demo,其格式是这样的:
{
"v":"4.10.1", # bodymovin的版本
"fr":60, # 动画帧数,这里表示1s执行60帧
"ip":0, # 动画起始帧数
"op":123, # op-ip 得到动画的总帧数
"w":400, "h":400, # 动画的宽和高,Lottie会对手机屏幕进行适配
"nm":"AndroidWave", # 名称
"ddd":0, # 3d
"assets":Array[], # 动画的资源文件
"layers":Array[] # 图层信息,要绘制的内容
}
开发者拿到此 json 文件后,怎么使用呢?这里以 lottie-android 为例:
implementation 'com.airbnb.android:lottie:3.7.0'
代码实现
在我们的 xml 文件之内,添加 LottieAnimationView(支持动态创建):
<com.airbnb.lottie.LottieAnimationView
android:id="@+id/animationView"
android:layout_width="match_parent"
android:layout_height="match_parent"
airbnb:lottie_fileName="RobotWave.json"
airbnb:lottie_autoPlay="true"
airbnb:lottie_loop="true"/>
在我们的代码中,直接使用即可,是不是非常方便:
private lateinit var lottieAnimaView: LottieAnimationView
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.lottie_layout)
lottieAnimaView = findViewById(R.id.animationView)
lottieAnimaView.playAnimation()
}
json源
lottie不仅可以加载 assets 文件夹内的 json,同时也支持:
src/main/res/raw 中的 json 动画 src/main/assets 下的 .zip 文件 json 或 zip 文件的 URL 来自任意位置的 json 字符串 来自Json InputStream
这里就不一一列举了,LottieAnimationView 提供了相应 set 方法,也可以使用 LottieCompositionFactory 提供的方法,仅以 url 为例说下:
private val url = "https://assets1.lottiefiles.com/packages/lf20_gygeywbl.json"
private var cacheKey : String? = null;
fun playAnimaFromUrl() {
LottieCompositionFactory.fromUrl(application, url, cacheKey)
.addListener() {lottieAnimaViewByUrl.setComposition(it)}
/* lottieAnimaViewByUrl.setAnimationFromUrl(url, cacheKey) */
lottieAnimaViewByUrl.playAnimation()
}
效果见下:
向注册监听或基本动画配置等API,就不在这里说了,用到的时候,查看 Lottie官网 即可。下面来说下 Lottie 是如何通过 JSON 让动画动起来的。
动画原理
1、JSON 到 LottieComposition
Lottie的第一步就是去解析 JSON, 将 json 各节点数据映射到 LottieComposition 类的字段内。这里以FromJsonString 为例,首先会将 JSON 转成 InputStream 流,并通过LottieCompositionFactory 这个简单工厂传入:
public void setAnimationFromJson(String jsonString, @Nullable String cacheKey) {
setAnimation(new ByteArrayInputStream(jsonString.getBytes()), cacheKey);
}
public void setAnimation(InputStream stream, @Nullable String cacheKey) {
setCompositionTask(LottieCompositionFactory.fromJsonInputStream(stream, cacheKey));
}
在这里会有一个缓冲读取机制,当无缓存可读时,将stream传入转为同步加载:
public static LottieTask<LottieComposition> fromJsonInputStream(final InputStream stream, @Nullable final String cacheKey) {
// 先在缓存中查找,有则直接返回 LottieTask<LottieComposition>
// 如果没有,则会在 LottieTask 内执行 call() 回调
return cache(cacheKey, new Callable<LottieResult<LottieComposition>>() {
@Override
public LottieResult<LottieComposition> call() {
return fromJsonInputStreamSync(stream, cacheKey);
}
});
}
在这里会将 InputStream 流转成 JsonReader 流,这样可以避免一次性全部加载到内存而引起 OOM 问题:
@WorkerThread
private static LottieResult<LottieComposition> fromJsonInputStreamSync(InputStream stream, @Nullable String cacheKey, boolean close) {
return fromJsonReaderSync(JsonReader.of(buffer(source(stream))), cacheKey);
}
@WorkerThread
public static LottieResult<LottieComposition> fromJsonReaderSync(com.airbnb.lottie.parser.moshi.JsonReader reader, @Nullable String cacheKey) {
return fromJsonReaderSyncInternal(reader, cacheKey, true);
}
最后通过 LottieCompositionMoshiParser.parse 方法从流中解析出完整的 LottieComposition 对象,LottieComposition 就包含了上面介绍的 Json 各节点数据:
private static LottieResult<LottieComposition> fromJsonReaderSyncInternal(
com.airbnb.lottie.parser.moshi.JsonReader reader, @Nullable String cacheKey, boolean close) {
LottieComposition composition = LottieCompositionMoshiParser.parse(reader);
if (cacheKey != null) {
LottieCompositionCache.getInstance().put(cacheKey, composition);
}
return new LottieResult<>(composition);
}
2、BaseLayer 到 LottieComposition
Lottie的第二步就是通过 LottieComposition 去生成各个层级的 BaseLayer,这里 LottieAnimationView 将composition 委托 lottieDrawable 去处理:
public void setComposition(@NonNull LottieComposition composition) {
...
boolean isNewComposition = lottieDrawable.setComposition(composition); ...
}
public boolean setComposition(LottieComposition composition) {
...
buildCompositionLayer(); ...
}
在这里会创建一个 CompositionLayer 对象,内部管理着各个层级的 Layer:
private void buildCompositionLayer() {
compositionLayer = new CompositionLayer(
this, LayerParser.parse(composition), composition.getLayers(), composition);
...
}
将 composition 内部的 Layer 集合,根据 type 生成对应 BaseLayer 具体类,并保存在 layerMap 集合内:
public CompositionLayer(LottieDrawable lottieDrawable, Layer layerModel, List<Layer> layerModels,
LottieComposition composition) {
LongSparseArray<BaseLayer> layerMap =
new LongSparseArray<>(composition.getLayers().size());
...
BaseLayer mattedLayer = null;
for (int i = layerModels.size() - 1; i >= 0; i--) {
Layer lm = layerModels.get(i);
BaseLayer layer = BaseLayer.forModel(lm, lottieDrawable, composition);
if (layer == null) {
continue;
}
layerMap.put(layer.getLayerModel().getId(), layer);
...
} ...
}
这是具体的 BaseLayer 类转换工具类:
static BaseLayer forModel(
Layer layerModel, LottieDrawable drawable, LottieComposition composition) {
switch (layerModel.getLayerType()) {
case SHAPE:
return new ShapeLayer(drawable, layerModel);
case PRE_COMP:
return new CompositionLayer(drawable, layerModel,
composition.getPrecomps(layerModel.getRefId()), composition);
case SOLID:
return new SolidLayer(drawable, layerModel);
case IMAGE:
return new ImageLayer(drawable, layerModel);
case NULL:
return new NullLayer(drawable, layerModel);
case TEXT:
return new TextLayer(drawable, layerModel);
case UNKNOWN:
default:
// Do nothing
Logger.warning("Unknown layer type " + layerModel.getLayerType());
return null;
}
}
3、播放动画
当点击播放时,LottieAnimationView 会委托 lottieDrawable,lottieDrawable 又会委托 animator 去执行 playAnimation 方法,animator 是 LottieValueAnimator 的对象,其控制着整个动画的进度和更新:
@MainThread
public void playAnimation() {
if (isShown()) {
lottieDrawable.playAnimation();
} ...
}
public void playAnimation() {
...
if (animationsEnabled() || getRepeatCount() == 0) {
animator.playAnimation();
} ...
}
notifyStart 方法会通知动画开始。setFrame 方法来设置当前帧数据,postFrameCallback 方法是核心,具体见下:
@MainThread
public void playAnimation() {
running = true;
// notifyStart 会通知 listener.onAnimationStart(this, isReverse);
notifyStart(isReversed());
setFrame((int) (isReversed() ? getMaxFrame() : getMinFrame()));
lastFrameTimeNs = 0;
repeatCount = 0;
postFrameCallback();
}
postFrameCallback 方法,会请求 VSYNC 信号:
protected void postFrameCallback() {
if (isRunning()) {
removeFrameCallback(false);
Choreographer.getInstance().postFrameCallback(this);
}
}
之后会回调到 LottieValueAnimator 内部的 doFrame 方法,我们能看到此时又会执行 postFrameCallback 方法,而 VSYNC 信号的间隔是是16.6ms,所以此方法会每过 16.6ms 就会更新下 frame 的值,并调用 notifyUpdate 通知 LottieDrawable 的 onAnimationUpdate 回调:
@Override public void doFrame(long frameTimeNanos) {
postFrameCallback();
if (composition == null || !isRunning()) {
return;
}
L.beginSection("LottieValueAnimator#doFrame");
long now = frameTimeNanos;
long timeSinceFrame = lastFrameTimeNs == 0 ? 0 : now - lastFrameTimeNs;
float frameDuration = getFrameDurationNs();
float dFrames = timeSinceFrame / frameDuration;
frame += isReversed() ? -dFrames : dFrames;
boolean ended = !MiscUtils.contains(frame, getMinFrame(), getMaxFrame());
frame = MiscUtils.clamp(frame, getMinFrame(), getMaxFrame());
lastFrameTimeNs = now;
notifyUpdate();
...
}
void notifyUpdate() {
for (ValueAnimator.AnimatorUpdateListener listener : updateListeners) {
listener.onAnimationUpdate(this);
}
}
LottieDrawable 的 onAnimationUpdate 回调后,会更新进度值:
private final ValueAnimator.AnimatorUpdateListener progressUpdateListener = new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
if (compositionLayer != null) {
compositionLayer.setProgress(animator.getAnimatedValueAbsolute());
}
}
};
LottieDrawable 通知每个 layers 去更新自己的进度值:
@Override public void setProgress(@FloatRange(from = 0f, to = 1f) float progress) {
super.setProgress(progress);
...
for (int i = layers.size() - 1; i >= 0; i--) {
layers.get(i).setProgress(progress);
}
}
BaseLayer 更新进度值后,会通知 onValueChanged :
void setProgress(@FloatRange(from = 0f, to = 1f) float progress) {
// Time stretch should not be applied to the layer transform.
transform.setProgress(progress);
...
if (matteLayer != null) {
// The matte layer's time stretch is pre-calculated.
float matteTimeStretch = matteLayer.layerModel.getTimeStretch();
matteLayer.setProgress(progress * matteTimeStretch);
}
for (int i = 0; i < animations.size(); i++) {
animations.get(i).setProgress(progress);
}
}
public void setProgress(@FloatRange(from = 0f, to = 1f) float progress) {
...
this.progress = progress;
if (keyframesWrapper.isValueChanged(progress)) {
notifyListeners();
}
}
public void notifyListeners() {
for (int i = 0; i < listeners.size(); i++) {
listeners.get(i).onValueChanged();
}
}
BaseLayer 随后会通知 lottieDrawable 去更新,之后会触发 LottieDrawable 的绘制方法 draw:
/* BaseLayer.java */
@Override public void onValueChanged() {
invalidateSelf();
}
private void invalidateSelf() {
lottieDrawable.invalidateSelf();
}
/* LottieDrawable.java*/
@Override public void draw(@NonNull Canvas canvas) {
if (safeMode) {
try {
drawInternal(canvas);
} catch (Throwable e) {
Logger.error("Lottie crashed in draw!", e);
}
} ...
}
LottieDrawable 之后又会去通知 compositionLayer 去绘制:
private void drawInternal(@NonNull Canvas canvas) {
if (!boundsMatchesCompositionAspectRatio()) {
drawWithNewAspectRatio(canvas);
} else {
drawWithOriginalAspectRatio(canvas);
}
}
private void drawWithOriginalAspectRatio(Canvas canvas) {
...
compositionLayer.draw(canvas, matrix, alpha);
...
}
最后 compositionLayer 又会去通知每个 layer,至此完成所有绘制工作。
public void draw(Canvas canvas, Matrix parentMatrix, int parentAlpha) {
...
if (!hasMatteOnThisLayer() && !hasMasksOnThisLayer()) {
drawLayer(canvas, matrix, alpha);
return;
}
...
}
转自:掘金 Battler
https://juejin.cn/post/6972947515641430024
- EOF -
看完本文有收获?请分享给更多人
推荐关注「安卓开发精选」,提升安卓开发技术
点赞和在看就是最大的支持❤️